/*
 * This file is part of the xOperator SPARQL/XMPP agent.
 * For further information see: http://xoperator.aksw.org
 * Copyright (C) 2007-2008  Jörg Unbehauen

 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.aksw.xoperator.aiml;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.aksw.xoperator.conf.AgentConfiguration;
import org.aksw.xoperator.sparql.SparqlEndpointFacade;
import org.aksw.xoperator.sparql.SparqlEndpointIdentifier;
import org.aksw.xoperator.sparql.SparqlEndpointType;
import org.aksw.xoperator.sparql.SparqlQuery;
import org.aksw.xoperator.sparql.SparqlResult;
import org.aksw.xoperator.sparql.http.RemoteSparqlEndpointConfiguration;
import org.aksw.xoperator.xmpp.JabberUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jivesoftware.smack.XMPPException;

import bitoflife.chatterbean.Match;

/**
 * This class is the interface between the groovy scripts and the xOperator. All
 * interactions should pass through this class for the sake of consistency and
 * maintainability.
 */
public class GroovyContext {

	private static Log log = LogFactory.getLog(GroovyContext.class);

	private Log glog = LogFactory.getLog("Groovy Template Log");

	private ChatContext chatCon;

	private AgentConfiguration acon;

	private SparqlEndpointFacade facade;

	private String composingId = RandomStringUtils.random(6);

	private Match match;

	public GroovyContext(Match match, AgentConfiguration aconf,
			SparqlEndpointFacade facade) {
		super();
		this.chatCon = ((ChatContext) match.getCallback().getContext());
		this.acon = aconf;
		this.facade = facade;
		this.match = match;
	}

	/**
	 * Determines what word(s) matched an asterisk in a template. For example if
	 * the input "What is a dog" matched against the template "WHAT IS A ", then
	 * <code>getMatch(1)</code> will return "dog"
	 * 
	 * @param position
	 *            the position of the asterisk used, starting with 1
	 * 
	 * @return The words that are matched by the asterisk.
	 */
	public String getMatch(int position) {
		String result = null;

		try {
			result = match.wildcard(
					bitoflife.chatterbean.Match.Section.PATTERN, position)
					.trim();
		} catch (Exception e) {
			log.error("an error occured while trying to fetch wildcard no"
					+ position, e);
		}

		return result;
	}

	/**
	 * same as GroovyContext.getMatch(int position)
	 * 
	 * @param position
	 *            same as GroovyContext.getMatch(int position)
	 * 
	 * @return same as GroovyContext.getMatch(int position)
	 */
	public String getWildcard(int position) {
		return getMatch(position);
	}

	/**
	 * convenience wrapper for queryForTable(....). queries just the built in
	 * store.
	 * 
	 * @param query
	 *            the SPARQL query string.
	 * 
	 * @return a list of SPARQLResultTableWrapper, containing one result, the
	 *         one from the local store.
	 */
	public List<SPARQLResultTableWrapper> queryLocally(String query) {
		return queryForTable(query, true, new ArrayList<String>(),
				new ArrayList<String>());
	}

	/**
	 * convenience wrapper for queryForTable(....). queries all remote SPARQL
	 * endpoints.
	 * 
	 * @param query
	 *            the SPARQL query string.
	 * 
	 * @return a list of SPARQLResultTableWrapper, for each store one table is
	 *         returned.
	 */
	public List<SPARQLResultTableWrapper> queryRemote(String query) {
		return queryForTable(query, false, null, new ArrayList<String>());
	}

	/**
	 * convenience wrapper for queryForTable(....). queries a SPARQL endpoint,
	 * specified by the URL
	 * 
	 * @param query
	 *            the SPARQL query string.
	 * 
	 * @param url
	 *            the full URL of the SPARQL endpoint, including all
	 *            paraemeters, except for the q
	 * 
	 * @return a list of SPARQLResultTableWrapper, for each store one table is
	 *         returned.
	 */
	public List<SPARQLResultTableWrapper> queryRemote(String query, String url) {
		List<String> urls = new ArrayList<String>();
		urls.add(url);
		return queryForTable(query, false, new ArrayList<String>(), new ArrayList<String>(),urls);
	}

	/**
	 * convenience wrapper for queryForTable(....). Sends a query to the just
	 * the list of stores.
	 * 
	 * @param query
	 *            the SPARQL query string.
	 * @param remoteStores
	 *            a list of all the stores that should be queried. If the list
	 *            is empty, no store will be asked, if null all stores will be
	 *            asked.
	 * 
	 * @return a list of SPARQLResultTableWrapper, for each store one table is
	 *         returned.
	 */
	public List<SPARQLResultTableWrapper> queryRemote(String query,
			List<String> remoteStores) {
		return queryForTable(query, false, remoteStores,
				new ArrayList<String>());
	}

	/**
	 * convenience wrapper for queryForTable(....). Sends a query to all peers
	 * via the p2p facilities.
	 * 
	 * @param query
	 *            the SPARQL query string.
	 * 
	 * @return a list of SPARQLResultTableWrapper, for each agent one table is
	 *         returned.
	 */
	public List<SPARQLResultTableWrapper> queryP2P(String query) {
		return queryForTable(query, false, new ArrayList<String>(), null);
	}

	/**
	 * fine grained querying of the xOperator system. A SPARQL query gets
	 * forwarded to the endpoints mentioned in the parameters.
	 * 
	 * @param query
	 *            the SPARQL query string.
	 * @param local
	 *            should the local store be used?
	 * @param stores
	 *            a list of all the stores that should be queried. If the list
	 *            is empty, no store will be asked, if null all stores will be
	 *            asked.
	 * @param agents
	 *            a list of jids of agents, that the query should be relayed to.
	 *            If the list is empty, no agent will be asked, if null all
	 *            agents will be asked.
	 * 
	 * @return a list of SPARQLResultTableWrapper, for each endpoint one table
	 *         is returned.
	 */
	public List<SPARQLResultTableWrapper> queryForTable(String query,
			boolean local, List<String> stores, List<String> agents) {
		return queryForTable(query, local, stores, agents, null);
	}
	/**
	 * fine grained querying of the xOperator system. A SPARQL query gets
	 * forwarded to the endpoints mentioned in the parameters.
	 * 
	 * @param query
	 *            the SPARQL query string.
	 * @param local
	 *            should the local store be used?
	 * @param stores
	 *            a list of all the stores that should be queried. If the list
	 *            is empty, no store will be asked, if null all stores will be
	 *            asked.
	 * @param agents
	 *            a list of jids of agents, that the query should be relayed to.
	 *            If the list is empty, no agent will be asked, if null all
	 *            agents will be asked.
	 *            
	 * @param urls
	 * 			a list of sparql endpoints, identified by their urls, that should be asked wih this query. Null or an empty list of strings will result that no additional store will be queried.
	 * 
	 * @return a list of SPARQLResultTableWrapper, for each endpoint one table
	 *         is returned.
	 */
	public List<SPARQLResultTableWrapper> queryForTable(String query,
			boolean local, List<String> stores, List<String> agents,List<String> urls) {
		SparqlQuery squery = new SparqlQuery(query);
		squery.setAgentsToAsk(stringToId(agents, SparqlEndpointType.P2P));
		List<SparqlEndpointIdentifier> storesToAsk = stringToId(stores, SparqlEndpointType.Remote);
		if(urls!=null&&!urls.isEmpty()){
			storesToAsk.addAll(stringToId(urls, SparqlEndpointType.URL));
		}
		squery.setStoresToAsk(storesToAsk);
		squery.setAskLocal(local);
		
		return executeQuery(squery);
	}

	private List<SPARQLResultTableWrapper> executeQuery(SparqlQuery squery) {
		facade.process(squery);
		List<SPARQLResultTableWrapper> results = new ArrayList<SPARQLResultTableWrapper>();

		for (SparqlResult oresult : squery.getResults().values()) {
			SPARQLResultTableWrapper wrappedResult = new SPARQLResultTableWrapper(
					oresult);
			results.add(wrappedResult);
		}

		return results;
	}

	/**
	 * converts a list of Strings into a list of SparqlEndpointIdentifier of the
	 * same type.
	 * 
	 * @param names
	 * @param type
	 * 
	 * @return
	 */
	private static List<SparqlEndpointIdentifier> stringToId(
			List<String> names, SparqlEndpointType type) {
		List<SparqlEndpointIdentifier> idents = null;

		if (names != null) {
			idents = new ArrayList<SparqlEndpointIdentifier>();

			for (String name : names) {
				idents.add(new SparqlEndpointIdentifier(type, name));
			}
		}

		return idents;
	}

	/**
	 * Method for getting all the endpoints that are known to the system.
	 * 
	 * @return a list of all the configured sparql endpoints.
	 */
	public List<SparqlEndpointIdentifier> getStores() {
		List<SparqlEndpointIdentifier> ids = new ArrayList<SparqlEndpointIdentifier>();

		for (RemoteSparqlEndpointConfiguration.SparqlEndpointConfigurationItem seid : acon
				.getRemoteSparqlEndpoints().getEndpoints().values()) {
			ids.add(seid.getId());
		}

		return ids;
	}

	/**
	 * A System log can be obtained here, writing into the instances log.
	 * 
	 * @return a org.apache.commons.logging.Log
	 */
	public Log getLog() {
		return glog;
	}

	/**
	 * Sends a message to the user that started this chat. Sends the passed
	 * String as a single message, please be careful with long strings.
	 * 
	 * @param message
	 *            The string to send
	 */
	public void sendMessageToUser(String message) {
		try {
			chatCon.getChat().sendMessage(message);
		} catch (XMPPException e) {
			log.error("Unable to send message: ", e);
		}
	}
	
	/**
	 * Sends a message to the user that started this chat. Sends the passed
	 * String as a single message, please be careful with long strings.
	 * 
	 * @param message
	 *            The string to send
	 */
	public void sendMessage(String message) {
		try {
			chatCon.getChat().sendMessage(message);
		} catch (XMPPException e) {
			log.error("Unable to send message: ", e);
		}
	}
	/**
	 * Sends the user on the other side of the chat a composing event to tell
	 * that the xOperator is working. This usually appears in the client, as if
	 * the xOperator is typing at the moment.
	 */
	public void startComposing() {
		try {
			this.composingId = RandomStringUtils.random(6);
			chatCon.getChat()
					.sendMessage(
							JabberUtils
									.composingNotification(this.composingId));
		} catch (XMPPException e) {
			log.error("Unable to send composing notification");
		}
	}

	/**
	 * Stops the client of the user to display the composing notification.
	 */
	public void stopComposing() {
		try {
			chatCon.getChat().sendMessage(
					JabberUtils
							.composingCancelNotification(this.composingId));
		} catch (XMPPException e) {
			log.error("Unable to send composing cancellation notification");
		}
	}

	/**
	 * A map to store values. the map is bound to the name of the user and will
	 * be available until the agent is stopped.
	 * 
	 * @return map related to the chat or user.
	 */
	public Map<String, Object> getUserContextMap() {
		synchronized (userContexts) {

			String jid = org.jivesoftware.smack.util.StringUtils
					.parseBareAddress(chatCon.getMessage().getFrom());
			// as there is a thread, look up the the chat Hashmap
			if (userContexts.get(jid) != null) {
				return userContexts.get(jid);
			} else {
				// no map was previously defined, so one is created an returned.
				Map<String, Object> userContextMap = new ConcurrentHashMap<String, Object>();
				userContexts.put(jid, userContextMap);
				return userContextMap;
			}
		}

	}

	/**
	 * a map to store objects in a application instance wide scope.
	 * 
	 * @return map realted to the instance of the agent.
	 */
	public Map<String, Object> getAgentContextMap() {
		return agentContext;
	}

	private static Map<String, Map<String, Object>> userContexts = new ConcurrentHashMap<String, Map<String, Object>>();

	private static Map<String, Object> agentContext = new ConcurrentHashMap<String, Object>();
}
